﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Threading.Tasks;
using DaveSexton.Labs.Properties;

namespace DaveSexton.Labs
{
	public abstract class ConsoleLab : Lab
	{
		#region Public Properties
		/// <summary>
		/// Gets or sets whether the lab should pause for user input after each experiment completes.
		/// The default value is <see langword="true" />.
		/// </summary>
		public bool PauseBetweenExperiments
		{
			get
			{
				return pauseBetweenExperiments;
			}
			set
			{
				pauseBetweenExperiments = value;
			}
		}
		#endregion

		#region Private / Protected
		private int userInputCursorLeft, userInputCursorTop;
		private int lastReplaceableTraceLineCursorLeft, lastReplaceableTraceLineCursorTop;
		private string lastUserInput;
		private string lastReplaceableTraceLine;
		private bool pauseBetweenExperiments = true;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLab" /> class for derived classes.
		/// </summary>
		protected ConsoleLab()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLab" /> class for derived classes.
		/// </summary>
		protected ConsoleLab(bool enabled)
			: base(enabled)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLab" /> class for derived classes.
		/// </summary>
		protected ConsoleLab(string labName)
			: base(labName)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(labName));
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLab" /> class for derived classes.
		/// </summary>
		protected ConsoleLab(string labName, bool enabled)
			: base(labName, enabled)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(labName));
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(!IsProxy);
		}

		protected ConsoleKeyInfo PressAnyKeyToContinue()
		{
			// Clear the buffer just in case the user accidentally pressed a key during a blocking operation.
			// This typically occurrs when a user presses a key to test whether the current operation is blocking, 
			// thus the user's intention was not for the key press to be consumed when the operation has completed.
			ConsoleLabController.FlushKeyboardBuffer();

			TraceLine();
			TraceLine(Resources.PressAnyKeyToContinue);

			return Console.ReadKey(intercept: true);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "It's easier for a lab to call it as an instance method, and not all languages support instance-style "
										+ "calls to static methods; e.g., F# requires static methods to be qualified, which breaks cross-platform "
										+ "generality unless the GUI labs also declare their implementations as static.")]
		protected ConsoleKeyInfo WaitForKey()
		{
			return Console.ReadKey(intercept: true);
		}

		private void UserInputPrompt(string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);

			Console.Write(format, args);

			userInputCursorLeft = Console.CursorLeft;
			userInputCursorTop = Console.CursorTop;
		}

		protected string UserInput(string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);
			Contract.Ensures(Contract.Result<string>() != null);

			UserInputPrompt(format, args);

			lastUserInput = Console.ReadLine();

			Contract.Assume(lastUserInput != null);

			return lastUserInput;
		}

		protected virtual string UserInput()
		{
			Contract.Ensures(Contract.Result<string>() != null);

			ClearUserInput();

			lastUserInput = Console.ReadLine();

			Contract.Assume(lastUserInput != null);

			return lastUserInput;
		}

		protected IEnumerable<ConsoleKeyInfo> UserInputKeys(string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);
			Contract.Ensures(Contract.Result<IEnumerable<ConsoleKeyInfo>>() != null);

			return UserInputKeysIterator(format, args);
		}

		private IEnumerable<ConsoleKeyInfo> UserInputKeysIterator(string format, params object[] args)
		{
			yield return UserInputKey(format, args);

			while (true)
			{
				yield return UserInputKey();
			}
		}

		protected ConsoleKeyInfo UserInputKey(string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);

			UserInputPrompt(format, args);

			var key = Console.ReadKey(intercept: false);

			TraceLine();

			lastUserInput = key.KeyChar.ToString();

			return key;
		}

		protected virtual ConsoleKeyInfo UserInputKey()
		{
			ClearUserInput();

			var key = Console.ReadKey(intercept: false);

			TraceLine();

			lastUserInput = key.KeyChar.ToString();

			return key;
		}

		protected Uri UserInputUrl(string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);
			Contract.Ensures(Contract.Result<Uri>() != null);

			return UserInputUrl(UriKind.RelativeOrAbsolute, format, args);
		}

		protected Uri UserInputUrl(UriKind kind, string format, params object[] args)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(format));
			Contract.Requires(args != null);
			Contract.Ensures(Contract.Result<Uri>() != null);
			Contract.Ensures(kind != UriKind.Absolute || Contract.Result<Uri>().IsAbsoluteUri);

			Uri url;
			while (!Uri.TryCreate(UserInput(format, args), kind, out url))
			{
				TraceLine();
				TraceError(Resources.InvalidUrl);
				TraceLine();
			}

			Contract.Assume(kind != UriKind.Absolute || url.IsAbsoluteUri);

			return url;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed",
			Justification = "All of the supported languages allow optional parameters.")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "It's easier for a lab to call it as an instance method, and not all languages support instance-style "
										+ "calls to static methods; e.g., F# requires static methods to be qualified, which breaks cross-platform "
										+ "generality unless the GUI labs also declare their implementations as static.")]
		protected IFile UserInputFileToOpen(
			string title = null,
			string filter = null,
			string initialDirectory = null)
		{
			return WindowsLab.UserInputFileToOpen(title, filter, initialDirectory, dispatcher: null);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed",
			Justification = "All of the supported languages allow optional parameters.")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "It's easier for a lab to call it as an instance method, and not all languages support instance-style "
										+ "calls to static methods; e.g., F# requires static methods to be qualified, which breaks cross-platform "
										+ "generality unless the GUI labs also declare their implementations as static.")]
		protected IList<IFile> UserInputFilesToOpen(
			string title = null,
			string filter = null,
			string initialDirectory = null)
		{
			Contract.Ensures(Contract.Result<IList<IFile>>() != null);
			Contract.Ensures(Contract.Result<IList<IFile>>().IsReadOnly);
			////Contract.Ensures(Contract.ForAll(Contract.Result<IList<IFile>>(), file => file != null));

			return WindowsLab.UserInputFilesToOpen(title, filter, initialDirectory, dispatcher: null);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed",
			Justification = "All of the supported languages allow optional parameters.")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "It's easier for a lab to call it as an instance method, and not all languages support instance-style "
										+ "calls to static methods; e.g., F# requires static methods to be qualified, which breaks cross-platform "
										+ "generality unless the GUI labs also declare their implementations as static.")]
		protected FileStream UserInputFileToSave(
			string title = null,
			string filter = null,
			string initialDirectory = null,
			string defaultExtension = null,
			bool canCreate = true)
		{
			return WindowsLab.UserInputFileToSave(title, filter, initialDirectory, defaultExtension, canCreate, dispatcher: null);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed",
			Justification = "All of the supported languages allow optional parameters.")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
			Justification = "It's easier for a lab to call it as an instance method, and not all languages support instance-style "
										+ "calls to static methods; e.g., F# requires static methods to be qualified, which breaks cross-platform "
										+ "generality unless the GUI labs also declare their implementations as static.")]
		protected IFolder UserInputFolder(
			string title = null,
			bool canCreate = true)
		{
			return WindowsLab.UserInputFolder(title, canCreate, dispatcher: null);
		}

		private void ClearUserInput()
		{
			if (!string.IsNullOrEmpty(lastUserInput))
			{
				MoveCursorToUserInput();

				Contract.Assume(lastUserInput != null);

				Console.Write(new string(' ', lastUserInput.Length));

				MoveCursorToUserInput();

				lastUserInput = null;
			}
		}

		private void MoveCursorToUserInput()
		{
			Contract.Ensures(lastUserInput == Contract.OldValue(lastUserInput));

			Console.SetCursorPosition(userInputCursorLeft, userInputCursorTop);
		}

		protected void TraceReplaceableLine(string message)
		{
			ClearLastReplaceableTraceLine();

			lastReplaceableTraceLineCursorLeft = Console.CursorLeft;
			lastReplaceableTraceLineCursorTop = Console.CursorTop;
			lastReplaceableTraceLine = message;

			TraceLine(message);
		}

		protected void TraceReplaceableLine(string format, params object[] args)
		{
			TraceReplaceableLine(Format(format, args));
		}

		private void ClearLastReplaceableTraceLine()
		{
			if (!string.IsNullOrEmpty(lastReplaceableTraceLine))
			{
				MoveCursorToLastReplaceableTraceLine();

				Contract.Assume(lastReplaceableTraceLine != null);

				var lines = lastReplaceableTraceLine.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

				for (int l = 0; l < lines.Length; l++)
				{
					var line = lines[l];

					Contract.Assume(line != null);

					var whitespace = new string(' ', line.Length);

					Console.Write(whitespace);

					if (l + 1 < lines.Length)
						Console.WriteLine();
				}

				MoveCursorToLastReplaceableTraceLine();

				lastReplaceableTraceLine = null;
			}
		}

		private void MoveCursorToLastReplaceableTraceLine()
		{
			Contract.Ensures(lastReplaceableTraceLine == Contract.OldValue(lastReplaceableTraceLine));

			Console.SetCursorPosition(lastReplaceableTraceLineCursorLeft, lastReplaceableTraceLineCursorTop);
		}

		public override void TraceDescription(string description)
		{
			TraceLine(ConsoleFormat.Wrap(Resources.LabDescriptionPrefix, description));
			TraceLine();
		}

		protected internal override async Task<bool> ExperimentStartingAsync(IList<IExperiment> experiments, int index)
		{
			if (IsRunningAllExperiments && PauseBetweenExperiments && index > 0)
			{
				TraceLine();
				TraceLine(Resources.PressAnyKeyToStartNextExperimentEscapeToCancel);
				TraceLine();

				var key = WaitForKey();

				if (key.Key == ConsoleKey.Escape)
				{
					return false;
				}
			}

			var experiment = experiments[index];

			Contract.Assume(experiment != null);

			if (await base.ExperimentStartingAsync(experiments, index).ConfigureAwait(false))
			{
				if (!string.IsNullOrEmpty(experiment.Description))
				{
					TraceLine();
					TraceDescription(experiment.Description);
				}

				return true;
			}

			return false;
		}
		#endregion
	}
}